After today's lesson you should be understand:
Today's exercise was created by Geoff Boeing for his Advanced Urban Analytics class. Thanks Geoff!
import networkx as nx
import numpy as np
import osmnx as ox
import pandas as pd
# configure OSMnx
ox.settings.log_console = True
# anytime you're curious what package version you have, use __version__
print(nx.__version__)
print(ox.__version__)
2.8.8 1.2.2
Networks let you represent structure and interaction among the components of a system. In analytics, they let you go beyond models that average across individuals/components or treat the population/system as a monolith. Networks are useful when the system's structure is nontrivial.
A network is a set of objects (called nodes or vertices) connected to each other by a set of connections (called edges or links). A graph is a mathematical model of a network: usually used synonymously. You can represent a graph as an adjacency matrix to use the tools of linear algebra to analyze it. You can also simulate dynamics and flows on it.
A trivial (simple) network is undirected, unweighted, and lacks self-loops or parallel edges. A nontrivial (complex) network may be directed and weighted and have self-loops and parallel edges. A spatial network is a network that is spatially embedded. That means its nodes and/or edges have locations in space. A spatial network is defined by both its geometry (positions, distances, angles, etc) and its topology (connections and configurations).
Examples:
We can analyze a network in various ways. To take street networks as an example, you can measure its compactness via intersection density, its connectedness via average node degree, or the relative importance of different nodes via centrality. Betweenness centrality measures what share of all shortest paths in a network pass through each node. Closeness centrality measures the average distance between a node and all other nodes in the network.
# create a random small-world graph of a social network
G = nx.watts_strogatz_graph(n=100, k=5, p=0.1, seed=0)
## Here we are creating a graph with 100 nodes, each node is connected to 5 other nodes, and the probability of rewiring is 0.1
## Rewiring means that the edge between two nodes is randomly reconnected to another node
nx.draw(G)
# how many nodes and edges?
print(len(G.nodes))
print(len(G.edges))
100 200
# assign random ages to each person in the network
randoms = np.random.randint(low=18, high=90, size=len(G.nodes))
ages = {node:age for node, age in zip(G.nodes, randoms)}
ages
{0: 19,
1: 28,
2: 67,
3: 30,
4: 56,
5: 68,
6: 60,
7: 24,
8: 68,
9: 20,
10: 68,
11: 52,
12: 32,
13: 39,
14: 26,
15: 39,
16: 26,
17: 63,
18: 18,
19: 52,
20: 54,
21: 61,
22: 53,
23: 48,
24: 21,
25: 55,
26: 39,
27: 73,
28: 52,
29: 52,
30: 88,
31: 80,
32: 77,
33: 58,
34: 57,
35: 78,
36: 71,
37: 33,
38: 39,
39: 39,
40: 50,
41: 35,
42: 70,
43: 48,
44: 29,
45: 83,
46: 70,
47: 64,
48: 69,
49: 68,
50: 54,
51: 33,
52: 57,
53: 55,
54: 81,
55: 54,
56: 47,
57: 44,
58: 39,
59: 23,
60: 38,
61: 56,
62: 84,
63: 65,
64: 88,
65: 82,
66: 89,
67: 82,
68: 32,
69: 73,
70: 28,
71: 32,
72: 50,
73: 20,
74: 61,
75: 72,
76: 75,
77: 56,
78: 70,
79: 48,
80: 29,
81: 68,
82: 64,
83: 62,
84: 34,
85: 54,
86: 72,
87: 19,
88: 68,
89: 40,
90: 57,
91: 40,
92: 81,
93: 83,
94: 50,
95: 47,
96: 23,
97: 80,
98: 81,
99: 49}
nx.set_node_attributes(G, values=ages, name='age')
`
# assign random "social distance" to each edge in the network
# social distance is the inverse of how often they hang out each year
hangout_counts = np.random.randint(low=1, high=100, size=len(G.edges))
distances = {edge: 1 / hangout_count for edge, hangout_count in zip(G.edges, hangout_counts)}
distances
{(0, 1): 0.010309278350515464,
(0, 99): 0.02857142857142857,
(0, 2): 0.02,
(0, 98): 0.017543859649122806,
(1, 2): 0.2,
(1, 3): 0.011235955056179775,
(1, 99): 0.018867924528301886,
(2, 3): 0.06666666666666667,
(2, 4): 0.016666666666666666,
(2, 17): 0.016129032258064516,
(3, 4): 1.0,
(3, 5): 0.011494252873563218,
(4, 5): 0.018867924528301886,
(4, 6): 0.014492753623188406,
(4, 92): 0.041666666666666664,
(5, 6): 0.04,
(5, 7): 0.14285714285714285,
(6, 7): 0.015151515151515152,
(6, 73): 0.014285714285714285,
(7, 8): 0.017543859649122806,
(7, 9): 0.03333333333333333,
(8, 9): 0.013333333333333334,
(8, 10): 0.010101010101010102,
(9, 10): 0.011235955056179775,
(9, 11): 0.012658227848101266,
(9, 27): 0.018518518518518517,
(10, 11): 0.041666666666666664,
(10, 15): 0.012345679012345678,
(11, 12): 0.018518518518518517,
(11, 13): 0.04,
(12, 13): 0.010638297872340425,
(12, 14): 0.014492753623188406,
(12, 32): 0.5,
(13, 14): 0.16666666666666666,
(13, 15): 0.034482758620689655,
(14, 15): 0.01694915254237288,
(14, 16): 0.16666666666666666,
(15, 16): 0.010526315789473684,
(15, 17): 0.017857142857142856,
(16, 17): 0.019230769230769232,
(16, 18): 0.025,
(17, 18): 0.016129032258064516,
(18, 19): 0.017857142857142856,
(18, 20): 0.015873015873015872,
(19, 20): 0.01020408163265306,
(19, 21): 0.022222222222222223,
(20, 21): 0.014705882352941176,
(20, 22): 0.1,
(20, 87): 0.013333333333333334,
(21, 22): 0.045454545454545456,
(21, 23): 0.03225806451612903,
(22, 23): 0.08333333333333333,
(22, 24): 0.05263157894736842,
(23, 24): 0.02,
(23, 86): 0.047619047619047616,
(24, 25): 0.041666666666666664,
(24, 54): 0.01282051282051282,
(24, 74): 0.011904761904761904,
(25, 26): 0.018518518518518517,
(25, 27): 0.015151515151515152,
(26, 27): 0.010309278350515464,
(26, 28): 0.02127659574468085,
(27, 28): 0.3333333333333333,
(28, 29): 0.037037037037037035,
(28, 30): 0.015384615384615385,
(29, 30): 0.029411764705882353,
(29, 31): 0.018518518518518517,
(30, 31): 0.017543859649122806,
(30, 32): 0.011764705882352941,
(31, 32): 0.022727272727272728,
(31, 33): 0.029411764705882353,
(32, 33): 0.02702702702702703,
(33, 34): 0.011235955056179775,
(33, 35): 0.010416666666666666,
(34, 35): 0.011904761904761904,
(34, 36): 0.022727272727272728,
(34, 97): 0.02040816326530612,
(35, 37): 0.023255813953488372,
(35, 92): 0.014925373134328358,
(35, 61): 0.024390243902439025,
(36, 37): 0.04,
(36, 38): 0.014285714285714285,
(37, 38): 0.027777777777777776,
(37, 39): 0.02564102564102564,
(38, 39): 0.015384615384615385,
(38, 40): 0.010416666666666666,
(39, 40): 0.011235955056179775,
(39, 41): 0.02040816326530612,
(40, 41): 0.02702702702702703,
(40, 42): 0.013513513513513514,
(40, 50): 0.011111111111111112,
(41, 42): 0.0136986301369863,
(41, 43): 0.025,
(42, 43): 0.011764705882352941,
(42, 44): 0.010101010101010102,
(43, 44): 0.011111111111111112,
(43, 45): 0.010869565217391304,
(44, 45): 0.017241379310344827,
(44, 46): 0.02631578947368421,
(45, 47): 0.05263157894736842,
(45, 72): 0.027777777777777776,
(46, 47): 0.03333333333333333,
(46, 48): 0.041666666666666664,
(47, 48): 0.03225806451612903,
(47, 49): 0.022727272727272728,
(48, 49): 0.01098901098901099,
(48, 50): 0.125,
(49, 50): 0.16666666666666666,
(49, 51): 0.16666666666666666,
(49, 63): 0.01818181818181818,
(50, 87): 0.010526315789473684,
(51, 52): 0.030303030303030304,
(51, 53): 0.03571428571428571,
(52, 53): 0.03225806451612903,
(52, 54): 0.012048192771084338,
(53, 54): 0.014084507042253521,
(53, 55): 0.010526315789473684,
(54, 55): 0.02702702702702703,
(54, 56): 0.012195121951219513,
(55, 56): 0.023809523809523808,
(55, 57): 0.012987012987012988,
(56, 57): 0.2,
(56, 58): 0.045454545454545456,
(57, 58): 0.012345679012345678,
(57, 59): 0.011627906976744186,
(58, 59): 0.037037037037037035,
(58, 60): 1.0,
(59, 60): 0.012048192771084338,
(59, 94): 0.047619047619047616,
(60, 61): 0.02040816326530612,
(60, 94): 0.027777777777777776,
(61, 62): 0.015873015873015872,
(62, 63): 0.014492753623188406,
(62, 64): 0.011627906976744186,
(63, 65): 0.014285714285714285,
(64, 65): 0.5,
(64, 66): 0.012987012987012988,
(65, 66): 0.03225806451612903,
(65, 67): 0.06666666666666667,
(66, 67): 0.03571428571428571,
(66, 68): 0.019230769230769232,
(67, 68): 0.0196078431372549,
(67, 69): 0.14285714285714285,
(68, 70): 0.045454545454545456,
(68, 84): 0.05,
(69, 70): 0.010101010101010102,
(69, 71): 0.011764705882352941,
(70, 72): 0.05263157894736842,
(70, 86): 0.02702702702702703,
(71, 72): 0.017241379310344827,
(71, 73): 0.019230769230769232,
(72, 73): 0.2,
(72, 74): 0.023255813953488372,
(73, 74): 1.0,
(73, 75): 0.016666666666666666,
(74, 75): 0.029411764705882353,
(75, 76): 0.011235955056179775,
(75, 77): 0.011235955056179775,
(76, 77): 0.023255813953488372,
(76, 78): 0.014285714285714285,
(77, 78): 0.0136986301369863,
(77, 79): 0.011494252873563218,
(78, 79): 0.047619047619047616,
(78, 80): 0.027777777777777776,
(78, 92): 0.01020408163265306,
(79, 80): 0.012195121951219513,
(79, 81): 0.013513513513513514,
(80, 81): 0.014925373134328358,
(80, 82): 0.02702702702702703,
(81, 82): 0.01282051282051282,
(81, 83): 0.011494252873563218,
(82, 83): 0.011111111111111112,
(82, 89): 0.25,
(83, 84): 0.010101010101010102,
(83, 85): 0.05,
(84, 85): 0.011494252873563218,
(84, 86): 0.021739130434782608,
(85, 86): 0.5,
(85, 87): 0.02040816326530612,
(86, 87): 0.01282051282051282,
(86, 88): 0.037037037037037035,
(87, 88): 0.5,
(88, 89): 0.014492753623188406,
(88, 90): 0.01020408163265306,
(89, 90): 0.034482758620689655,
(89, 91): 0.058823529411764705,
(90, 91): 0.025,
(90, 92): 0.017241379310344827,
(91, 92): 0.030303030303030304,
(91, 93): 0.014925373134328358,
(93, 94): 0.010869565217391304,
(93, 95): 0.020833333333333332,
(94, 95): 0.023809523809523808,
(94, 96): 0.020833333333333332,
(95, 96): 0.010526315789473684,
(95, 97): 0.02857142857142857,
(96, 97): 0.024390243902439025,
(96, 98): 0.015625,
(97, 99): 0.015873015873015872,
(98, 99): 0.027777777777777776}
nx.set_edge_attributes(G, values=distances, name='distance')
# view the nodes and optionally show their attribute data
G.nodes#(data=True)
NodeView((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99))
# view the edges and optionally show their attribute data
# these are undirected edges, and there cannot be parallel edges
G.edges#(data=True)
EdgeView([(0, 1), (0, 99), (0, 2), (0, 98), (1, 2), (1, 3), (1, 99), (2, 3), (2, 4), (2, 17), (3, 4), (3, 5), (4, 5), (4, 6), (4, 92), (5, 6), (5, 7), (6, 7), (6, 73), (7, 8), (7, 9), (8, 9), (8, 10), (9, 10), (9, 11), (9, 27), (10, 11), (10, 15), (11, 12), (11, 13), (12, 13), (12, 14), (12, 32), (13, 14), (13, 15), (14, 15), (14, 16), (15, 16), (15, 17), (16, 17), (16, 18), (17, 18), (18, 19), (18, 20), (19, 20), (19, 21), (20, 21), (20, 22), (20, 87), (21, 22), (21, 23), (22, 23), (22, 24), (23, 24), (23, 86), (24, 25), (24, 54), (24, 74), (25, 26), (25, 27), (26, 27), (26, 28), (27, 28), (28, 29), (28, 30), (29, 30), (29, 31), (30, 31), (30, 32), (31, 32), (31, 33), (32, 33), (33, 34), (33, 35), (34, 35), (34, 36), (34, 97), (35, 37), (35, 92), (35, 61), (36, 37), (36, 38), (37, 38), (37, 39), (38, 39), (38, 40), (39, 40), (39, 41), (40, 41), (40, 42), (40, 50), (41, 42), (41, 43), (42, 43), (42, 44), (43, 44), (43, 45), (44, 45), (44, 46), (45, 47), (45, 72), (46, 47), (46, 48), (47, 48), (47, 49), (48, 49), (48, 50), (49, 50), (49, 51), (49, 63), (50, 87), (51, 52), (51, 53), (52, 53), (52, 54), (53, 54), (53, 55), (54, 55), (54, 56), (55, 56), (55, 57), (56, 57), (56, 58), (57, 58), (57, 59), (58, 59), (58, 60), (59, 60), (59, 94), (60, 61), (60, 94), (61, 62), (62, 63), (62, 64), (63, 65), (64, 65), (64, 66), (65, 66), (65, 67), (66, 67), (66, 68), (67, 68), (67, 69), (68, 70), (68, 84), (69, 70), (69, 71), (70, 72), (70, 86), (71, 72), (71, 73), (72, 73), (72, 74), (73, 74), (73, 75), (74, 75), (75, 76), (75, 77), (76, 77), (76, 78), (77, 78), (77, 79), (78, 79), (78, 80), (78, 92), (79, 80), (79, 81), (80, 81), (80, 82), (81, 82), (81, 83), (82, 83), (82, 89), (83, 84), (83, 85), (84, 85), (84, 86), (85, 86), (85, 87), (86, 87), (86, 88), (87, 88), (88, 89), (88, 90), (89, 90), (89, 91), (90, 91), (90, 92), (91, 92), (91, 93), (93, 94), (93, 95), (94, 95), (94, 96), (95, 96), (95, 97), (96, 97), (96, 98), (97, 99), (98, 99)])
# calculate the shortest path between two nodes
path1 = nx.shortest_path(G, source=0, target=50)
path1
[0, 2, 17, 18, 20, 87, 50]
# calculate the shortest weighted path between two nodes
path2 = nx.shortest_path(G, source=0, target=50, weight='distance')
path2
[0, 2, 17, 18, 20, 87, 50]
# calculate node betweenness centrality across the network
bc = nx.betweenness_centrality(G, weight='distance')
pd.Series(bc).describe()
count 100.000000 mean 0.048192 std 0.042308 min 0.000000 25% 0.014327 50% 0.039683 75% 0.069316 max 0.200577 dtype: float64
# now it's your turn
# try changing the social distance between our people, then recompute a shortest path
There is nothing explicitly spatial about the graph above. Although it models people and their relationships, it captures nothing about their positions in space. Now let's look at real-world spatial networks.
OSMnx lets you download, model, analyze, and visualize street networks (and any other spatial data) anywhere in the world from OpenStreetMap.
OSMnx is built on top of GeoPandas, NetworkX, and matplotlib and interacts with OpenStreetMap’s APIs to:
More info:
# download/model a street network for some city then visualize it
place = 'Ithaca, New York, USA'
G = ox.graph_from_place(place, network_type='rail')
fig, ax = ox.plot_graph(G)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In [37], line 3 1 # download/model a street network for some city then visualize it 2 place = 'Ithaca, New York, USA' ----> 3 G = ox.graph_from_place(place, network_type='rail') 4 fig, ax = ox.plot_graph(G) File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/graph.py:351, in graph_from_place(query, network_type, simplify, retain_all, truncate_by_edge, which_result, buffer_dist, clean_periphery, custom_filter) 348 utils.log("Constructed place geometry polygon(s) to query API") 350 # create graph using this polygon(s) geometry --> 351 G = graph_from_polygon( 352 polygon, 353 network_type=network_type, 354 simplify=simplify, 355 retain_all=retain_all, 356 truncate_by_edge=truncate_by_edge, 357 clean_periphery=clean_periphery, 358 custom_filter=custom_filter, 359 ) 361 utils.log(f"graph_from_place returned graph with {len(G)} nodes and {len(G.edges)} edges") 362 return G File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/graph.py:432, in graph_from_polygon(polygon, network_type, simplify, retain_all, truncate_by_edge, clean_periphery, custom_filter) 429 poly_buff, _ = projection.project_geometry(poly_proj_buff, crs=crs_utm, to_latlong=True) 431 # download the network data from OSM within buffered polygon --> 432 response_jsons = downloader._osm_network_download(poly_buff, network_type, custom_filter) 434 # create buffered graph from the downloaded data 435 bidirectional = network_type in settings.bidirectional_network_types File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/downloader.py:534, in _osm_network_download(polygon, network_type, custom_filter) 532 osm_filter = custom_filter 533 else: --> 534 osm_filter = _get_osm_filter(network_type) 536 response_jsons = [] 538 # create overpass settings string File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/downloader.py:105, in _get_osm_filter(network_type) 103 osm_filter = filters[network_type] 104 else: # pragma: no cover --> 105 raise ValueError(f'Unrecognized network_type "{network_type}"') 107 return osm_filter ValueError: Unrecognized network_type "rail"
OSMnx geocodes the query "Ithaca, New York, USA" to retrieve the place boundaries of that city from the Nominatim API, retrieves the drivable street network data within those boundaries from the Overpass API, constructs a graph model, then simplifies/corrects its topology such that nodes represent intersections and dead-ends and edges represent the street segments linking them.
# look at the first 10 nodes: these are OSM IDs
list(G.nodes)[0:10]
[213409446, 213409480, 213409892, 213409894, 213409896, 213409899, 213409959, 213409964, 213410337, 213410339]
# look at the first 10 edges: u, v, key
list(G.edges)[0:10]
[(213409446, 213409480, 0), (213409446, 213467056, 0), (213409446, 213467003, 0), (213409480, 213409446, 0), (213409480, 213449564, 0), (213409480, 213447279, 0), (213409892, 213472435, 0), (213409892, 213515843, 0), (213409892, 213409894, 0), (213409894, 213409896, 0)]
type(G)
networkx.classes.multidigraph.MultiDiGraph
OSMnx models all networks as NetworkX MultiDiGraph objects. You can convert to:
# convert your graph to node and edge GeoPandas GeoDataFrames
gdf_nodes, gdf_edges = ox.graph_to_gdfs(G)
gdf_nodes.head()
| y | x | street_count | highway | geometry | |
|---|---|---|---|---|---|
| osmid | |||||
| 213409446 | 42.448787 | -76.518574 | 3 | NaN | POINT (-76.51857 42.44879) |
| 213409480 | 42.449783 | -76.517811 | 3 | NaN | POINT (-76.51781 42.44978) |
| 213409892 | 42.444591 | -76.499413 | 3 | NaN | POINT (-76.49941 42.44459) |
| 213409894 | 42.445959 | -76.500512 | 3 | NaN | POINT (-76.50051 42.44596) |
| 213409896 | 42.446884 | -76.501260 | 4 | stop | POINT (-76.50126 42.44688) |
gdf_edges.head()
| osmid | name | highway | oneway | reversed | length | geometry | ref | maxspeed | lanes | bridge | access | junction | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| u | v | key | |||||||||||||
| 213409446 | 213409480 | 0 | 20181025 | Vinegar Hill | residential | False | False | 130.393 | LINESTRING (-76.51857 42.44879, -76.51854 42.4... | NaN | NaN | NaN | NaN | NaN | NaN |
| 213467056 | 0 | 345317460 | Hector Street | primary | False | False | 504.350 | LINESTRING (-76.51857 42.44879, -76.51851 42.4... | NY 79 | 30 mph | NaN | NaN | NaN | NaN | |
| 213467003 | 0 | 345317460 | Hector Street | primary | False | True | 292.613 | LINESTRING (-76.51857 42.44879, -76.51873 42.4... | NY 79 | 30 mph | NaN | NaN | NaN | NaN | |
| 213409480 | 213409446 | 0 | 20181025 | Vinegar Hill | residential | False | True | 130.393 | LINESTRING (-76.51781 42.44978, -76.51798 42.4... | NaN | NaN | NaN | NaN | NaN | NaN |
| 213449564 | 0 | 20187426 | Cliff Street | primary | False | False | 609.383 | LINESTRING (-76.51781 42.44978, -76.51851 42.4... | NY 96 | NaN | 2 | NaN | NaN | NaN |
You can create a graph from node/edge GeoDataFrames, as long as gdf_nodes is indexed by osmid and gdf_edges is multi-indexed by u, v, key (following normal MultiDiGraph structure). This allows you to load graph node/edge shapefiles or GeoPackage layers as GeoDataFrames then convert to a MultiDiGraph for graph analytics.
# convert node/edge GeoPandas GeoDataFrames to a NetworkX MultiDiGraph
G2 = ox.graph_from_gdfs(gdf_nodes, gdf_edges, graph_attrs=G.graph)
print(len(G2.nodes))
print(len(G2.edges))
621 1690
# now it's your turn
# download a graph of a different (small-ish) town, then plot it
# get our study site's geometry
gdf = ox.geocode_to_gdf(place)
gdf_proj = ox.project_gdf(gdf)
geom_proj = gdf_proj['geometry'].iloc[0]
geom_proj
# what size area does our study site cover in square meters?
area_m = geom_proj.area
area_m
15745858.508835593
# project the graph (automatically) then check its new CRS
G_proj = ox.project_graph(G)
G_proj.graph['crs']
<Derived Projected CRS: +proj=utm +zone=18 +ellps=WGS84 +datum=WGS84 +unit ...> Name: unknown Axis Info [cartesian]: - E[east]: Easting (metre) - N[north]: Northing (metre) Area of Use: - undefined Coordinate Operation: - name: UTM zone 18N - method: Transverse Mercator Datum: World Geodetic System 1984 - Ellipsoid: WGS 84 - Prime Meridian: Greenwich
# show some basic stats about the (projected) network
ox.basic_stats(G_proj, area=area_m, clean_int_tol=10)
{'n': 621,
'm': 1690,
'k_avg': 5.442834138486313,
'edge_length_total': 227324.6080000004,
'edge_length_avg': 134.51160236686414,
'streets_per_node_avg': 3.0096618357487923,
'streets_per_node_counts': {0: 0, 1: 82, 2: 2, 3: 368, 4: 167, 5: 1, 6: 1},
'streets_per_node_proportions': {0: 0.0,
1: 0.1320450885668277,
2: 0.00322061191626409,
3: 0.5925925925925926,
4: 0.2689210950080515,
5: 0.001610305958132045,
6: 0.001610305958132045},
'intersection_count': 539,
'street_length_total': 122698.14400000003,
'street_segment_count': 915,
'street_length_avg': 134.0963322404372,
'circuity_avg': 1.0565395120223608,
'self_loop_proportion': 0.003278688524590164,
'clean_intersection_count': 484,
'node_density_km': 39.438941970139865,
'intersection_density_km': 34.23122338471077,
'edge_density_km': 14437.104707402268,
'street_density_km': 7792.407376907997,
'clean_intersection_density_km': 30.73824140667906}
More stats documentation
### UPDATE THESE FILE PATHS!
# save graph to disk as geopackage (for GIS) or GraphML file (for Gephi etc)
ox.save_graph_geopackage(G, filepath='./data/mynetwork.gpkg')
ox.save_graphml(G, filepath='./data/mynetwork.graphml')
Here we plot the street network and color its edges (streets) by their relative closeness centrality.
# convert graph to line graph so edges become nodes and vice versa
edge_centrality = nx.closeness_centrality(nx.line_graph(G))
nx.set_edge_attributes(G, edge_centrality, 'edge_centrality')
# color edges in original graph with centralities from line graph
ec = ox.plot.get_edge_colors_by_attr(G, 'edge_centrality', cmap='inferno')
fig, ax = ox.plot_graph(G, edge_color=ec, edge_linewidth=2, node_size=0)
# impute missing edge speeds then calculate edge (free-flow) travel times
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)
# get the nearest network nodes to two lat/lng points
orig = ox.nearest_nodes(G, -76.484735,42.450335)
dest = ox.nearest_nodes(G, -76.487419,42.440510)
# find the shortest path between these nodes, minimizing travel time, then plot it
route = ox.shortest_path(G, orig, dest, weight='travel_time')
fig, ax = ox.plot_graph_route(G, route, node_size=0)
# how long is our route in meters?
edge_lengths = ox.utils_graph.get_route_edge_attributes(G, route, 'length')
sum(edge_lengths)
1529.3419999999996
# how far is it between these two nodes as the crow flies (haversine)?
ox.distance.great_circle_vec(G.nodes[orig]['y'], G.nodes[orig]['x'],
G.nodes[dest]['y'], G.nodes[dest]['x'])
1180.4507452286598
# now it's your turn
# how circuitous is this route?
# try plotting it differently: change the colors and node/edge sizes
make queries less ambiguous to help the geocoder out, if it's not finding what you're looking for
# you can make query an unambiguous dict to help the geocoder find it
place = {'city' : 'San Francisco',
'state' : 'California',
'country': 'USA'}
G = ox.graph_from_place(place, network_type='drive', truncate_by_edge=True)
fig, ax = ox.plot_graph(G, figsize=(10, 10), node_size=0, edge_color='y', edge_linewidth=0.2)
# you can get networks anywhere in the world
G = ox.graph_from_place('Sinalunga, Italy', network_type='all')
fig, ax = ox.plot_graph(G, node_size=0, edge_linewidth=0.5)
# or get network by address, coordinates, bounding box, or any custom polygon
# ...useful when OSM just doesn't already have a polygon for the place you want
sibley_hall = (42.450768,-76.484696)
one_mile = 1609 #meters
G = ox.graph_from_point(sibley_hall, dist=one_mile, network_type='drive')
fig, ax = ox.plot_graph(G, node_size=0)
# now it's your turn
# create a graph of your hometown then calculate the shortest path between two points of your choice
...like rail or electric grids or even the canals of Venice and Amsterdam, using the custom_filter parameter. See the Overpass Query Language documentation for query usage details.
# get NY subway rail network
G = ox.graph_from_place('New York City, New York, USA',
retain_all=False, truncate_by_edge=True, simplify=True,
custom_filter='["railway"~"subway"]')
fig, ax = ox.plot_graph(G, node_size=0, edge_color='c', edge_linewidth=0.2)
Use the geometries module to download entities, such as local amenities, points of interest, or building footprints, and turn them into a GeoDataFrame.
# get all building footprints in some neighborhood
place = 'West Village, New York, New York, USA'
tags = {'building': True}
gdf = ox.geometries_from_place(place, tags)
gdf.shape
(2259, 109)
gdf.explore(color='heritage')
gdf['nycdoitt:bin']
element_type osmid
node 368043320 NaN
368043476 NaN
368043569 NaN
368043579 NaN
368043604 NaN
...
relation 3357450 1010421
3365138 1012047
3365139 1012125
3365140 1012154
4462495 1077817
Name: nycdoitt:bin, Length: 2259, dtype: object
gdf.columns[:50]
Index(['addr:state', 'building', 'ele', 'gnis:county_name', 'gnis:feature_id',
'gnis:import_uuid', 'gnis:reviewed', 'name', 'source', 'geometry',
'railway', 'wheelchair', 'addr:housenumber', 'addr:postcode',
'addr:street', 'amenity', 'tourism', 'addr:city', 'access', 'nodes',
'height', 'nycdoitt:bin', 'leisure', 'phone', 'website', 'atm', 'brand',
'brand:wikidata', 'fax', 'opening_hours', 'short_name',
'building:levels', 'roof:levels', 'start_date', 'building:material',
'operator', 'operator:wikidata', 'operator:wikipedia', 'roof:shape',
'wikidata', 'heritage', 'heritage:operator', 'historic',
'nrhp:inscription_date', 'nrhp:nhl', 'protect_class',
'protection_object', 'ref:nrhp', 'wikipedia', 'internet_access'],
dtype='object')
fig, ax = ox.plot_footprints(gdf, figsize=(3, 3))
# get all parks and bus stops in some neighborhood
tags = {'leisure': 'park',
'highway': 'bus_stop'}
gdf = ox.geometries_from_place(place, tags)
gdf.shape
# restaurants near the empire state buildings
address = '350 5th Ave, New York, NY 10001'
tags = {'amenity': 'restaurant'}
gdf = ox.geometries_from_address(address, tags=tags, dist=500)
gdf[['name', 'cuisine', 'geometry']].dropna().head()
# now it's your turn
# find all the rail stations around downtown LA
# hint, the tag is railway and the value is station: https://wiki.openstreetmap.org/wiki/Tag:railway%3Dstation
If you're interested in more network analysis, check out: